Spring5源码解析-Spring中的Context loader
我们已经知道,应用程序上下文是Spring管理的bean所在的容器。但是我们依然要问一个问题:这个上下文是如何创建的?那么在这篇文章中我们来探讨这个问题。
在第一部分中,会说下在Spring的应用程序上下文中所谓的上下文加载器(context loader)是什么。在第二部分,我们会讨论这个加载器的代码细节。最后一部分,老规矩,写我们自己的一个自定义的loader。在继续之前,需要说一下,loader(加载器) 将根据web application和dispatcher servlet来结合进行分析。其实这也是很多人一碰到源码就像无头苍蝇,不知道从何而起了,刚开始放下所有,从大体去思考该如何入手,这里对设计模式了解就很重要了,还有,源码的类注释很重要,不多说,接着走。
什么是Spring的上下文加载器(context loader)?
见名知意,上下文加载程序负责构建应用程序上下文。我们可以通过org.springframework.web.context.ContextLoaderListener的实例来对其分析(从我之前的设计模式的文章可以看到,Spring通过观察者模式,其实我自己总结的是电影院模式,声音和画面通过broadcaster发送到listener,listener再调用相应的adapter来处理,所以,这里就直接从listener来找了),它继承并扩展了同一个包下的ContextLoader
类。同时还实现了javax.servlet.ServletContextListener接口。该接口旨在接收有关servlet上下文中更改变化的通知。只有当它们在(WEB-INF/web.xml
)中注册时,这个接口的实现才能接收这些通知。
在Spring Web应用程序中,会在servlet上下文创建时调用上下文加载程序(context loader
)。之后,开始初始化根Web应用程序上下文(Root WebApplicationContext
)。Root非常重要,因为在加载的时候,可以创建两个或更多的上下文。第一个,也是最重要的,定义了整个bean的生存空间,被称为应用程序上下文(application context)。另一个是servlet应用程序上下文,其包含更多的是面向Web的元素,比如控制器(controllers)或视图解析器。然而我们需要记住的是,servlet
的上下文是根应用程序上下文(Root WebApplicationContext
)的子集,也就是父子容器一说。这意味着servlet
可以从根应用程序上下文继承所有的bean。这就是为什么你可以在根配置文件中定义一些常见资源(例如:services,这也是我们的Spring xml配置文件为什么要分service和MVC两个的原因),并通过两个不同的servlet进行共享的原因。但是在另一方面,根应用程序上下文不能获取到特定于servlet的bean,看过我的逃逸分析的应该都清楚了吧。
我们可以将注意力拉回到关于上下文加载器的两个作用上:
将根Web应用程序上下文(
Root WebApplicationContext
)绑定到调度程序特定的上下文中自动创建上下文(程序员不需要编写任何东西来使上下文工作)
Spring的上下文加载器详解
我们已经了解了上下文加载器的作用。现在,我们来更详细地介绍这其中的细节。web上下文加载器(context loader)类位于org.springframework.web.context包中。主类是ContextLoaderListener
,它扩展了ContextLoader
类。同时实现了ServletContextListener
接口。
在上下文创建时调用的方法是public void contextInitialized(ServletContextEvent event)。它通过传递给它所接收到的servlet上下文(从事件参数获取event.getServletContext()
)来调用ContextLoader
的initWebApplicationContext
方法。initWebApplicationContext
方法进行的第一个操作是检查是否有另一个根上下文存在。如果至少存在另一个,则抛出IllegalStateException
,并且初始化失败。否则,它继续初始化org.springframework.web.context.WebApplicationContext实例。如果初始化的实例实现了ConfigurableWebApplicationContext
接口,则在设置当前应用程序上下文之前,加载器将进行一些设置服务(父上下文,应用程序上下文,servlet上下文等),并通过上下文的refresh()
方法来准备bean,这已经在关于应用程序上下文的文章中介绍过了。
org.springframework.web.context.ContextLoaderListener:
1 | /** |
org.springframework.web.context.ContextLoader:
1 | /** |
ContextLoaderListener
中第二个我们需要关注的方法是public void contextDestroyed(ServletContextEvent event)。每当加载程序的上下文关闭时都会调用它。这个方法干了两件事情:
- 通过
ContextLoader
中的closeWebApplicationContext()
,它关闭应用程序上下文。通过ConfigurableWebApplicationContext close()
方法完成上下文关闭。上下文的销毁的过程其实就是销毁bean和关闭bean工厂,此处参考org.springframework.context.support.AbstractApplicationContext中的源码,下面相关部分已贴出。 - 调用ContextCleanupListener.cleanupAttributes(event.getServletContext()),它将查找当前servlet上下文的所有实现org.springframework.beans.factory.DisposableBean接口的对象。之后,将调用它们的destroy()方法,以销毁不再使用的bean。
org.springframework.web.context.ContextLoaderListener:
1 | /** |
org.springframework.web.context.ContextLoader:
1 | /** |
org.springframework.context.support.AbstractApplicationContext:
1 | /** |
在Spring Web应用程序中实现上下文加载程序
想象一下,你希望在系统的所有用户之间共享一个信息。你可以用传统的方式做到这一点,也可以使用你定义的上下文加载器。我们通过写一些简单的代码来达到这个目的。还有一个想要实现的功能会涉及多个上下文。我们的应用程序将同时处理guest
和connected
两种形式(请同时看下面源码)。可以看到他们的网页的URL匹配规则不一样。使用connected的用户将能够访问与guest规则下以.chtml扩展名结尾的相同的页面,也就是所谓的交集。需要说的是,他们不会共享相同的信息(两个不一样的上下文当然不会一样了)。还不懂的话看下面源码,对于这两者,我们将分别 指定两个servlet上下文。你会看到,因为它,访问connected用户将不会与访问guest共享相同的bean。
我们将从web.xml
文件开始,请对比上面说的:
1 | <!--?xml version="1.0" encoding="UTF-8"?--> |
两个指定的servlet的bean配置文件几乎相同。唯一的区别是connected-servlet.xml包含一个没有与guest servlet共享的bean的定义。这个bean的名字是secretData:
1 | <bean id="secretData" class="com.migo.secret.SecretData"> |
神秘豆的内容主要由setter和toString方法组成:
1 | public class SecretData { |
其他Java代码也很简单。在CustomizedContextLoader
中,我们重写contextInitialized
方法来放置共享servlet
的上下文属性:名字叫webappVersion
。该属性是一个随机数,用于证明根应用程序上下文的加载程序仅被调用一次:
1 | public class CustomizedContextLoader extends ContextLoaderListener { |
之后,我们传递给用来处理访问网址的TestController
:
1 |
|
测试的时候,首先输入http://localhost:8080/test.chtml,然后输入http://localhost:8080/test.html。然后通过查看日志:
1 | [CustomizedContextLoader] Loading context |
首先,将一个信息(“Version set into servlet’s context :”+version)放在servlet上下文中,并由两个servlet上下文继承。第二点是bean的可见性。Guest
的servlet
没有看到secretData bean
,因为它仅在connected
(connected-servlet.xml)的配置中被定义。
总结
第一部分涉及了这个加载器的两个主要角色:将根Web应用程序上下文(Root WebApplicationContext
)绑定到调度程序特定的上下文中并自动创建上下文。接下来,我们分析了关于上下文加载程序的代码的要点所涉及的细节,如所实现的接口和主要方法的细节实现。最后一部分是我们自定义扩展本地上下文加载器,然后对bean和servlet的属性继承方面进行一些测试。